﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Raona.Code.Contracts.EventArguments;
using Raona.Code.Model;
using System.Reflection;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using System.ServiceModel;
using System.Threading;
using Raona.Code.Contracts.Inspection;

namespace Raona.Code.Inspection
{
    public class AssemblyOperationContractInspector : IOperationContractInspector
    {
        #region Events (Public)

        /// <summary>
        /// Event fired on completion of every step in the inspection process
        /// </summary>
        public event EventHandler<ProgressEventArgs> ProgressChanged;

        /// <summary>
        /// Event fired when the inspection process finishes
        /// </summary>
        public event EventHandler<ProgressResultEventArgs> ProcessFinished;

        #endregion

        #region Instance Fields (Private)

        /// <summary>
        /// The assembly to inspect
        /// </summary>
        private string contractsAssemblyPath = null;

        /// <summary>
        /// The source of cancellation tokens for the inspection process
        /// </summary>
        private CancellationTokenSource cancellationTokenSource = null;

        // The code inspection result is a collection of namespaces
        // containing the interfaces found
        private ConcurrentDictionary<string, Namespace> namespaces = null;

        #endregion

        #region Instance Constructors (Public)

        public AssemblyOperationContractInspector(string contractsAssemblyPath)
        {
            this.namespaces = new ConcurrentDictionary<string, Namespace>();
            this.contractsAssemblyPath = contractsAssemblyPath;
        }

        #endregion

        #region Instance Properties (Public)

        /// <summary>
        /// The collection of namespaces resulting of the inspection process
        /// </summary>
        public IEnumerable<Namespace> Namespaces
        {
            get
            {
                return this.namespaces.Values.AsEnumerable();
            }
        }

        #endregion

        #region Instance Methods (Public)

        /// <summary>
        /// Runs an inspection process synchronously
        /// </summary>
        /// <param name="clientBase">The base class for the client class</param>
        /// <param name="serviceContractsNamespace">The xml namespace for the generated contracts</param>
        public Dictionary<string, Namespace> GetInterfaces(string clientBase, string serviceContractsNamespace)
        {
            Dictionary<string, Namespace> result = new Dictionary<string,Namespace>();

            Task<Namespace>[] tasks = this.CreateInspectionTasks(clientBase, serviceContractsNamespace);
            Task.WaitAll(tasks);
            this.GatherInspectionTasksResults(tasks);

            foreach (KeyValuePair<string, Namespace> item in this.namespaces)
            {
                result.Add(item.Key, item.Value);
            }

            return result;
        }

        /// <summary>
        /// Initiates the inspection process asynchronously
        /// </summary>
        /// <param name="clientBase">The base class for the client class</param>
        /// <param name="serviceContractsNamespace">The xml namespace for the generated contracts</param>
        public void GetInterfacesAsync(string clientBase, string serviceContractsNamespace)
        {
            Task<Namespace>[] tasks = this.CreateInspectionTasks(clientBase, serviceContractsNamespace);
            // Gather the data for all namespaces
            Task.Factory.ContinueWhenAll(tasks, (_) =>
                {
                    this.GatherInspectionTasksResults(tasks);
                    this.ReportResult();
                },
                this.cancellationTokenSource.Token
            );
        }

        /// <summary>
        /// Cancels the inspection process
        /// </summary>
        public void CancelGetInterfaces()
        {
            if (this.cancellationTokenSource != null)
            {
                this.cancellationTokenSource.Cancel();
            }
        }

        /// <summary>
        /// IDisposable implementation
        /// </summary>
        public void Dispose()
        {
            if (this.cancellationTokenSource != null)
            {
                this.cancellationTokenSource.Dispose();
                this.cancellationTokenSource = null;
            }
        }

        #endregion

        #region Instance Methods (Private)

        private Task<Namespace>[] CreateInspectionTasks(string clientBase, string serviceContractsNamespace)
        {
            this.cancellationTokenSource = new CancellationTokenSource();

            Assembly contractsAssembly = Assembly.LoadFrom(this.contractsAssemblyPath);
            // Get all public types in the provided assembly
            Type[] types = contractsAssembly.GetExportedTypes();
            // Get interface types only
            IEnumerable<Type> interfaces = types.Where(t => t.IsInterface && t.IsPublic);
            // Get interfaces marked as ServiceContract
            IEnumerable<Type> serviceContracts =
                    from i in interfaces
                    let attributes = i.GetCustomAttributes(typeof(ServiceContractAttribute), true).Cast<ServiceContractAttribute>()
                    where attributes.Count() > 0
                    select i;
            // Group interfaces by namespace
            IEnumerable<IGrouping<string, Type>> serviceContractsNamespaceGroups = serviceContracts.GroupBy(i => i.Namespace);
            int count = serviceContractsNamespaceGroups.Count();

            // Create a task to inspect each namespace found
            Task<Namespace>[] tasks = new Task<Namespace>[count];
            for (int i = 0; i < count; i++)
            {
                Tuple<string, IGrouping<string, Type>, string> state = new Tuple<string, IGrouping<string, Type>, string>(clientBase, serviceContractsNamespaceGroups.ElementAt(i), serviceContractsNamespace);
                tasks[i] = Task<Namespace>.Factory.StartNew(this.CreateNamespace, state, this.cancellationTokenSource.Token);
            }
            return tasks;
        }

        private void GatherInspectionTasksResults(Task<Namespace>[] tasks)
        {
            // tasks[i].Result contains the resulting namespace
            for (int i = 0; i < tasks.Length; i++)
            {
                this.namespaces.TryAdd(tasks[i].Result.Name, tasks[i].Result);
            }
        }

        private Namespace CreateNamespace(object state)
        {
            ParallelOptions options = new ParallelOptions { CancellationToken = this.cancellationTokenSource.Token };

            Tuple<string, IGrouping<string, Type>, string> arguments = (Tuple<string, IGrouping<string, Type>, string>)state;
            IGrouping<string, Type> grouping = arguments.Item2;
            string serviceContractsNamespace = arguments.Item3;
            Namespace nameSpace = new Namespace { Name = grouping.Key };
            ConcurrentBag<Interface> interfaces = new ConcurrentBag<Interface>();
            Parallel.ForEach(grouping, options, t =>
            {
                Interface itf = new Interface { Name = t.Name, ClientBaseName = arguments.Item1, ServiceContractNamespace = serviceContractsNamespace };
                this.ReportProgress(itf);
                IEnumerable<MethodInfo> methodInfos = t.GetMethods().Where(m => m.GetCustomAttributes(typeof(OperationContractAttribute), false).Length > 0);
                itf.AddMethods(methodInfos.Select(m =>
                    new Method
                    {
                        Name = m.Name,
                        OverloadedName = this.GetOverloadedName(m),
                        ReturnType = this.GetTypeName(m.ReturnType),
                        Parameters = m.GetParameters().Select(p => new Parameter { Name = p.Name, Type = this.GetTypeName(p.ParameterType) }).ToList()
                    }));
                interfaces.Add(itf);
            });
            nameSpace.Interfaces.AddRange(interfaces);
            return nameSpace;
        }

        private string GetOverloadedName(MethodInfo methodInfo)
        {
            OperationContractAttribute attribute = methodInfo.GetCustomAttributes(typeof(OperationContractAttribute), false)[0] as OperationContractAttribute;
            return string.IsNullOrEmpty(attribute.Name) ? methodInfo.Name : attribute.Name;
        }

        private string GetTypeName(Type type)
        {
            string typeName = null;
            if (type.IsGenericTypeDefinition)
            {
                int argumentCount = type.GetGenericArguments().Length;
                typeName = type.ToString().Replace(string.Format("`{0}[", argumentCount), "<").Replace("]", ">");
            }
            else
            {
                typeName = 
                    type == typeof(void) ? "void" : 
                    type.IsEnum && type.IsNested ? type.FullName.Replace(type.Namespace, null).Replace(".", null).Replace("+", ".") : type.FullName;
            }
            return typeName;
        }

        private void ReportProgress(Interface itf)
        {
            if (this.ProgressChanged != null)
            {
                this.ProgressChanged(this, new ProgressEventArgs { Message = string.Format("Processing interface: {0}", itf.Name) });
            }
        }

        private void ReportResult()
        {
            Dictionary<string, Namespace> result = new Dictionary<string,Namespace>();
            foreach (KeyValuePair<string, Namespace> item in this.namespaces)
            {
                result.Add(item.Key, item.Value);
            }
            this.ReportResult(new ProgressResultEventArgs(result));
        }

        private void ReportResult(Exception error)
        {
            this.ReportResult(new ProgressResultEventArgs(error));
        }

        private void ReportResult(ProgressResultEventArgs resultEventArgs)
        {
            if (this.ProcessFinished != null)
            {
                this.ProcessFinished(this, resultEventArgs);
            }
        }
        #endregion
    }
}
